package uu.tools.core;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MoldMap<K, V> {
    private final Map<K, MoldValue<V>> map = new HashMap<>();
    private ReentrantReadWriteLock readWriteLock;
    private Lock readLock;
    private Lock writeLock;
    private long expiration = 0L;

    private MoldMap(Builder builder) {
        this.expiration = builder.expiration;
        this.readWriteLock = builder.lock == null ? new ReentrantReadWriteLock() : builder.lock;
        readLock = readWriteLock.readLock();
        writeLock = readWriteLock.writeLock();
    }

    public static Builder builder() {
        return new Builder();
    }

    public int size() {
        readLock.lock();
        try {
            int size = 0;
            for (MoldValue<V> value : map.values()) {
                if (unwrap(value) != null) {
                    ++size;
                }
            }
            return size;
        } finally {
            readLock.unlock();
        }
    }

    public Set<K> keys() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

    public void clear() {
        writeLock.lock();
        try {
            map.clear();
        } finally {
            writeLock.unlock();
        }
    }

    public void disinfect() {
        writeLock.lock();
        try {
            map.entrySet().removeIf(item -> unwrap(item.getValue()) == null);
        } finally {
            writeLock.unlock();
        }
    }

    public boolean isEmpty() {
        readLock.lock();
        try {
            for (MoldValue<V> value : map.values()) {
                if (unwrap(value) != null) {
                    return false;
                }
            }
            return true;
        } finally {
            readLock.unlock();
        }
    }

    public V get(K k) {
        boolean moldy = false;
        readLock.lock();
        try {
            MoldValue<V> v = map.get(k);
            if (v == null) {
                return null;
            } else if (moldy = v.isMoldy()) {
                return null;
            }
            return v.getValue();
        } finally {
            readLock.unlock();
            if (moldy) {
                writeLock.lock();
                try {
                    MoldValue<V> v = map.get(k);
                    if (v != null && v.isMoldy()) {
                        map.remove(k);
                    }
                } finally {
                    writeLock.unlock();
                }
            }
        }
    }

    public V put(K k, V v, long duration) {
        // 考虑动态随机
        if (ThreadLocalRandom.current().nextInt(888) == 1) {
            disinfect();
        }
        writeLock.lock();
        try {
            return unwrap(map.put(k, wrap(v, duration)));
        } finally {
            writeLock.unlock();
        }
    }

    public V remove(K k) {
        writeLock.lock();
        try {
            return unwrap(map.remove(k));
        } finally {
            writeLock.unlock();
        }
    }

    public V getOrDefault(K k, V dv) {
        V v;
        return (v = get(k)) == null ? dv : v;
    }

    public boolean containsKey(K k) {
        return get(k) != null;
    }

    public V put(K k, V v) {
        return put(k, v, expiration);
    }

    public V put(K k, V v, Duration duration) {
        return put(k, v, duration.toMillis());
    }

    public void runWrite(Runnable runnable) {
        writeLock.lock();
        try {
            runnable.run();
        } finally {
            writeLock.unlock();
        }
    }

    public void runRead(Runnable runnable) {
        readLock.lock();
        try {
            runnable.run();
        } finally {
            readLock.unlock();
        }
    }

    private V unwrap(MoldValue<V> v) {
        return v == null || v.isMoldy() ? null : v.getValue();
    }

    private MoldValue<V> wrap(V v, long duration) {
        return new MoldValue<>(v, duration == 0L ? 0L : duration + System.currentTimeMillis());
    }

    @Data
    @AllArgsConstructor
    static class MoldValue<V> {
        private V value;
        private long time;

        /* 是否过期 */
        boolean isMoldy() {
            return time != 0L && time < System.currentTimeMillis();
        }
    }

    public static final class Builder {
        private long expiration;
        private ReentrantReadWriteLock lock;

        public MoldMap build() {
            return new MoldMap(this);
        }

        public Builder expiration(Duration duration) {
            this.expiration = duration.toMillis();
            return this;
        }

        public Builder lock(ReentrantReadWriteLock lock) {
            this.lock = lock;
            return this;
        }

        private Builder() {
        }
    }
}
